Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/kob 66 angebote stadtweit #24

Open
wants to merge 45 commits into
base: dev
Choose a base branch
from

Conversation

ThomasAFink
Copy link
Member

@ThomasAFink ThomasAFink commented May 26, 2023

Description

New Feature Offers
Moved Admin Info Button
Refactored

Reference

Issues #XXX

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced new components for managing offers, including NewOffer, EditOffer, and TheOffer.
    • Added a BackButton component to various pages for improved navigation.
    • Implemented hooks for fetching offers with useGetOfferListItems, useGetEditableOffers, and useGetOffer.
  • Improvements

    • Enhanced UI styling across multiple components with consistent color attributes.
    • Updated routing configuration to reflect new naming conventions and improved clarity.
  • Bug Fixes

    • Improved error handling in various components to ensure better user feedback during operations.
  • Chores

    • Cleaned up and removed deprecated files and components related to the "Angebote" feature.

# Conflicts:
#	frontend/src/core/core.router.ts
#	frontend/src/features/admin/features/the-contact-points/api/ContactPointsManipulationClient.ts
#	frontend/src/features/admin/features/the-contact-points/components/SaveUpdateButton.vue
#	frontend/src/features/admin/features/the-contact-points/middelware/useContactPoints.ts
#	frontend/src/features/admin/features/the-contact-points/the-contact-points-overview.vue
#	frontend/src/features/admin/the-admin-overview.vue
#	frontend/src/features/the-app-bar/features/the-drawer-main/the-drawer-main-store.module.ts
#	frontend/src/features/the-main/the-main.vue
#	frontend/src/features/the-unterstuetzungsfinder/features/the-contact-points/the-contact-points.routes.ts
#	frontend/src/features/the-unterstuetzungsfinder/features/the-contact-points/the-contact-points.vue
@ThomasAFink ThomasAFink requested a review from clemens-code May 26, 2023 14:40
@ThomasAFink ThomasAFink self-assigned this May 26, 2023
@ThomasAFink
Copy link
Member Author

@coderabbitai full review

Copy link

coderabbitai bot commented Nov 29, 2024

Walkthrough

The changes in this pull request involve significant updates to the frontend codebase, primarily focusing on routing, state management, component enhancements, and the introduction of new features related to offers. Key modifications include renaming and restructuring routes, the removal of the "Angebote" module, and the addition of new components for managing offers. Various components have been updated for improved styling, and several new API functions have been introduced for handling offers. The overall structure and functionality of many components remain intact, with enhancements aimed at consistency and clarity.

Changes

File Path Change Summary
frontend/src/core/core.router.ts Modified import statements for consistency; renamed theAngeboteRoutes to theOfferRoutes and adminExperienceMoreRoutes to adminAdditionalRoutes; added adminOffersRoutes.
frontend/src/core/core.store.ts Removed THE_ANGEBOTE_MODULE_NAME, TheAngeboteModuleState, and theAngeboteStoreModule; deleted theAngeboteModuleState from RootState.
frontend/src/core/services/downloads/privacypolicy.vue Updated template with new CSS classes for styling; no changes to script or style sections.
frontend/src/core/translations/de-DE.json Added new offers section under Admin; updated addNewDescription entry in contentItems.
frontend/src/features/admin/components/middleware/useGetAdminUserInfoText.ts Updated query key in useQuery from ['adminInfo'] to ['adminInfoText'].
frontend/src/features/admin/features/the-additional/commons/AddTextItemDialog.vue Added color="secondary" attribute to input fields; no other changes.
frontend/src/features/admin/features/the-additional/commons/DeleteTextItemDialog.vue Changed button and snackbar colors to error for deletion actions; no changes to logic.
frontend/src/features/admin/features/the-additional/commons/EditTextItemDialog.vue Added color="secondary" to input fields; updated validation logic.
frontend/src/features/admin/features/the-additional/commons/TextList.vue Changed delete button color to error; no changes to logic.
frontend/src/features/admin/features/the-additional/features/api/TextItemManipulationClient.ts Added conditional check for link parameter in deleteTextItem function.
frontend/src/features/admin/features/the-additional/features/the-conflict-prevention/the-conflict-prevention.vue Restructured template layout; added BackButton component.
frontend/src/features/admin/features/the-additional/features/the-downloads/the-downloads.vue Adjusted layout structure; updated input field color.
frontend/src/features/admin/features/the-additional/features/the-faq/the-faq.vue Modified template structure; added color="secondary" to input fields.
frontend/src/features/admin/features/the-additional/features/the-glossar/the-glossar.vue Updated input field color to secondary; no changes to logic.
frontend/src/features/admin/features/the-additional/features/the-leadership-cooperation/the-leadership-cooperation.vue Restructured layout; added BackButton.
frontend/src/features/admin/features/the-additional/the-additional-overview-routes.ts Renamed constants for "Experience More" feature to "Additional".
frontend/src/features/admin/features/the-additional/the-additional-overview.vue Updated component name and replaced BackButton.
frontend/src/features/admin/features/the-contact-points/components/AddContactDialog.vue Added color="secondary" to email input field.
frontend/src/features/admin/features/the-contact-points/components/AddLinkDialog.vue Added color="secondary" to link title and URL fields.
frontend/src/features/admin/features/the-contact-points/components/DeleteButton.vue Updated button and dialog colors to error.
frontend/src/features/admin/features/the-contact-points/components/EditContactPoint.vue Added color="secondary" to input fields; updated variable types and watchers.
frontend/src/features/admin/features/the-contact-points/components/NewContactPoint.vue Added color="secondary" to input fields; updated watcher for adminUserInfo.
frontend/src/features/admin/features/the-contact-points/middelware/useContactPoints.ts Enhanced error handling for mutation functions; added headers for useCreateNewContactPoint.
frontend/src/features/admin/features/the-contact-points/the-contact-points-overview.vue Simplified conditional rendering logic; updated variable types.
frontend/src/features/admin/features/the-offers/api/OffersManipulationClient.ts Introduced functions for managing offers: postOffer, putOffer, deleteOffer.
frontend/src/features/admin/features/the-offers/components/DeleteButton.vue Added confirmation dialog for deleting offers.
frontend/src/features/admin/features/the-offers/components/EditOffer.vue Introduced component for editing offers with validation and error handling.
frontend/src/features/admin/features/the-offers/components/NewOffer.vue Introduced component for creating new offers with validation and error handling.
frontend/src/features/admin/features/the-offers/the-offers-overview.vue Introduced component for managing offers with loading and error handling.
frontend/src/features/admin/features/the-offers/the-offers.routes.ts Added route configuration for offers management.
frontend/src/features/commons/api/OfferGetClient.ts Introduced functions for fetching offers from the API.
frontend/src/features/commons/types/Offer.ts Introduced TypeScript interfaces Offer and OfferListItem.
frontend/src/features/the-unterstuetzungsfinder/features/the-contact-points/the-contact-points.routes.ts Added constant for contact points informational text.
frontend/src/features/the-unterstuetzungsfinder/features/the-contact-points/the-contact-points.vue Updated rendering logic and import statements for route metadata.

Poem

In the meadow, changes bloom,
With routes and offers making room.
A button here, a style anew,
The admin's tasks, now clearer too.
Hopping forward, we embrace the light,
In CodeRabbit's world, all feels just right! 🐇✨

Warning

Rate limit exceeded

@ThomasAFink has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 25 minutes and 45 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between d298770 and 957d7c9.


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments.

🛑 Comments failed to post (67)
frontend/src/features/commons/mixins/formatter.mixin.ts (1)

8-8: 💡 Codebase verification

Type mismatch detected in formatDate usage

The test file shows that formatDate is being called with a Date object (new Date("2021-12-31")), but the method signature expects a string parameter. This is a breaking change that needs to be addressed.

  • frontend/src/features/commons/mixins/formatter.spec.ts: Test passes a Date object but method expects string
  • Current implementation uses moment(date) which accepts both string and Date objects, suggesting the type restriction might be unnecessary
🔗 Analysis chain

Verify all callers pass string dates.

The parameter type change from Date to string could cause type errors in existing code.

Let's verify all usages of the formatDate method:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Find all calls to formatDate to verify type compatibility

# Search for components importing the formatter mixin
echo "Components using formatter mixin:"
rg -l "formatter\.mixin"

# Search for formatDate method calls
echo -e "\nformatDate method calls:"
rg "formatDate\(" -A 2

Length of output: 770


Script:

#!/bin/bash
# Let's check the test file to understand the actual type being passed
cat frontend/src/features/commons/mixins/formatter.spec.ts

Length of output: 934

frontend/src/features/commons/api/OfferGetClient.ts (1)

8-10: ⚠️ Potential issue

Add input validation and use URL template literals.

The current implementation has potential security implications:

  1. Direct string concatenation in URLs can be risky
  2. No validation of the id parameter

Apply these improvements:

+const isValidOfferId = (id: string): boolean => {
+    return /^[a-zA-Z0-9-_]+$/.test(id);
+};

 export const getOfferById = (id: string) => {
+    if (!id || !isValidOfferId(id)) {
+        throw new Error('Invalid offer ID');
+    }
-    return httpGetJson<Offer>("/offers/" + id);
+    return httpGetJson<Offer>(`/offers/${id}`);
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

const isValidOfferId = (id: string): boolean => {
    return /^[a-zA-Z0-9-_]+$/.test(id);
};

export const getOfferById = (id: string) => {
    if (!id || !isValidOfferId(id)) {
        throw new Error('Invalid offer ID');
    }
    return httpGetJson<Offer>(`/offers/${id}`);
};
frontend/src/features/commons/types/Offer.ts (2)

12-20: 🛠️ Refactor suggestion

Refactor interface hierarchy to reduce duplication

The OfferListItem interface largely duplicates the Offer interface with only one difference in the departments property. This could lead to maintenance issues.

Consider using interface extension or utility types:

-export interface OfferListItem {
-  id?: string;
-  title: string;
-  description: string;
-  startDate?: string;
-  endDate?: string;
-  imageLink: string;
-  departments: string[];
-}
+export interface OfferListItem extends Omit<Offer, 'departments'> {
+  departments: string[]; // Required in list items
+}

Committable suggestion skipped: line range outside the PR's diff.


1-20: 💡 Codebase verification

Consider using existing Department enum and add documentation

Based on the codebase analysis, there's an existing Department enum in frontend/src/features/the-unterstuetzungsfinder/features/the-contact-points/types/department.type.ts that defines valid department values (RIT, POR, DIR, NONE). The interfaces are being used in multiple places including offer filtering based on user departments.

  • Consider reusing the existing Department enum for type safety
  • Add JSDoc documentation to explain the difference between optional and required departments in the interfaces
  • Ensure consistency with the department values used in AdminUserInfo

Example:

import { Department } from '../the-unterstuetzungsfinder/features/the-contact-points/types/department.type';

/** 
 * Represents an offer entity for creation/editing.
 * Departments are optional during draft/creation.
 */
export interface Offer {
  id?: string;
  title: string;
  description: string;
  startDate?: string;
  endDate?: string;
  imageLink: string;
  departments?: Department[];
}

/**
 * Represents an offer in list views.
 * Departments are required for filtering by user's department.
 */
export interface OfferListItem extends Omit<Offer, 'departments'> {
  departments: Department[];
}
🔗 Analysis chain

Add documentation and consider type safety for departments

The relationship between Offer and OfferListItem and their usage patterns aren't immediately clear. Additionally, the departments field could benefit from stronger typing.

Consider these improvements:

  1. Add JSDoc documentation explaining the purpose and usage of each interface
  2. Use an enum or union type for departments
  3. Consider using a discriminated union if these represent different states of an offer

Example:

/** Valid department values for offers */
export enum Department {
  HR = 'HR',
  IT = 'IT',
  // ... add other valid departments
}

/** 
 * Represents a complete offer entity.
 * Used when creating or editing individual offers.
 */
export interface Offer {
  // ... existing fields
  departments?: Department[];
}

/**
 * Represents an offer in a list view.
 * Requires departments to enable filtering and grouping in list displays.
 */
export interface OfferListItem extends Omit<Offer, 'departments'> {
  departments: Department[];
}

Let's verify the current usage of departments:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for department values to understand valid options
rg -A 1 "departments.*=" --type typescript

Length of output: 79


Script:

#!/bin/bash
# Search for department values using ripgrep without file type filter
rg -A 1 "departments.*="

# Also search for any usage of these interfaces
rg -l "interface (Offer|OfferListItem)"

# Search for department-related string literals
rg "'[A-Z][A-Za-z]+'" --type ts

Length of output: 372


Script:

#!/bin/bash
# Search for any usage of departments in the codebase
rg -A 3 "departments" --type-add 'ts:*.{ts,tsx}' --type ts

# Search for files importing or using these interfaces
rg -A 2 "import.*Offer(ListItem)?.*from" --type-add 'ts:*.{ts,tsx}' --type ts

Length of output: 5199


Script:

#!/bin/bash
# Search for department-related code in admin user info
rg -A 3 "department" frontend/src/features/admin/components/userinformation/

# Look for any enum or type definitions related to departments
ast-grep --pattern 'enum $_Department {
  $$$
}'

# Search for department string literals in the codebase
rg '"[A-Z][A-Za-z]+"' --type-add 'ts:*.{ts,tsx}' --type ts

Length of output: 7097

frontend/src/core/services/downloads/privacypolicy.vue (1)

4-8: ⚠️ Potential issue

Validate download URLs for security

The download URL is used directly from the backend response without validation, which could be a security risk if the backend is compromised.

Consider adding URL validation:

 <script lang="ts">
 import {defineComponent} from "vue";
 import {PageType} from "@/features/the-additional/common/model/PageType";
 import {useGetAdditionalContent} from "@/features/the-additional/common/middleware/AdditionalPageService";
+
+// Allowed domains for downloads
+const ALLOWED_DOMAINS = ['your-domain.com'];
+
+function isValidDownloadUrl(url: string): boolean {
+  try {
+    const parsedUrl = new URL(url);
+    return ALLOWED_DOMAINS.some(domain => parsedUrl.hostname.endsWith(domain));
+  } catch {
+    return false;
+  }
+}

 export default defineComponent ({
   name: "PrivacyPolicy",
   setup() {
     const {isLoading, isError, data, error} = useGetAdditionalContent(PageType.PRIVACY_POLICY);
+    
+    const safeDownloadUrl = computed(() => {
+      if (!data.value?.textItemView[0]?.link) return null;
+      return isValidDownloadUrl(data.value.textItemView[0].link)
+        ? data.value.textItemView[0].link
+        : null;
+    });

     return {
       isLoading,
       isError,
-      item: data,
+      item: safeDownloadUrl,
       error
     };
   }
 });
 </script>

Committable suggestion skipped: line range outside the PR's diff.

frontend/src/features/admin/features/the-offers/components/NewOfferListItem.vue (1)

31-41: 🛠️ Refactor suggestion

Enhance prop validation and type safety

The props lack required validation and specific type definitions. Consider adding proper validation and TypeScript types.

 props: {
   label: {
     type: Object as () => I18nLabel,
+    required: true,
+    validator: (value: I18nLabel) => {
+      return typeof value.addNewTitle === 'string' 
+        && typeof value.addNewDescription === 'string';
+    }
   },
   setIsAddNew: {
     type: Function,
+    required: true,
+    validator: (value: Function) => typeof value === 'function'
   },
   disabled: {
     type: Boolean,
     default: false
   }
-}
+},
+emits: ['click']

Committable suggestion skipped: line range outside the PR's diff.

frontend/src/features/commons/middleware/useGetOffers.ts (2)

19-25: 🛠️ Refactor suggestion

Add documentation and defensive checks

This function contains important business logic and should be documented. Also, consider adding defensive checks for null/undefined values.

+/**
+ * Filters offers based on user's admin status and department.
+ * @param listItems - List of offers to filter
+ * @param adminUserInfo - Admin user information containing permissions
+ * @returns Filtered list of offers that the user can edit
+ */
 const filterForEditableOffers = (listItems: OfferListItem[], adminUserInfo: AdminUserInfo) => {
+    if (!listItems?.length || !adminUserInfo) {
+        return [];
+    }
     if (adminUserInfo.isCentralAdmin) {
         return listItems
     } else {
-        return listItems.filter(it => it.departments.includes(adminUserInfo.department));
+        return listItems.filter(it => it.departments?.includes(adminUserInfo.department));
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

/**
 * Filters offers based on user's admin status and department.
 * @param listItems - List of offers to filter
 * @param adminUserInfo - Admin user information containing permissions
 * @returns Filtered list of offers that the user can edit
 */
const filterForEditableOffers = (listItems: OfferListItem[], adminUserInfo: AdminUserInfo) => {
    if (!listItems?.length || !adminUserInfo) {
        return [];
    }
    if (adminUserInfo.isCentralAdmin) {
        return listItems
    } else {
        return listItems.filter(it => it.departments?.includes(adminUserInfo.department));
    }
}

27-36: 🛠️ Refactor suggestion

Refactor promise chain and improve error handling

The current implementation could be improved by:

  1. Using async/await for better readability
  2. Adding proper error handling
  3. Making the query key more specific
 export const useGetEditableOffers = () =>
     useQuery<OfferListItem[]>(
-        ['adminListItems'],
+        ['offers', 'editable'],
-        () => getOffers()
-            .then((listItems) => {
-                return getAdminUserInfo().then(adminInfo => {
-                    return filterForEditableOffers(listItems, adminInfo)
-                })
-            })
+        async () => {
+            try {
+                const [listItems, adminInfo] = await Promise.all([
+                    getOffers(),
+                    getAdminUserInfo()
+                ]);
+                return filterForEditableOffers(listItems, adminInfo);
+            } catch (error) {
+                console.error('Failed to fetch editable offers:', error);
+                throw error;
+            }
+        }
     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

export const useGetEditableOffers = () =>
    useQuery<OfferListItem[]>(
        ['offers', 'editable'],
        async () => {
            try {
                const [listItems, adminInfo] = await Promise.all([
                    getOffers(),
                    getAdminUserInfo()
                ]);
                return filterForEditableOffers(listItems, adminInfo);
            } catch (error) {
                console.error('Failed to fetch editable offers:', error);
                throw error;
            }
        }
    )
frontend/src/features/admin/features/the-offers/middelware/useOffers.ts (3)

23-30: ⚠️ Potential issue

Fix parameter naming and enhance deletion safety.

The parameter name contactPointToDelete suggests this code was copied from another context. Also, consider adding validation before deletion.

 export const useDeleteOffer = () => useMutation({
-    mutationFn: async (contactPointToDelete: Offer) => {
-        if (contactPointToDelete.id) {
-            return deleteOffer(contactPointToDelete);
+    mutationFn: async (offerToDelete: Offer) => {
+        if (!offerToDelete.id) {
+            throw new Error('Cannot delete offer: ID is required');
         }
-        throw new Error('ID is missing');
+        // Validate that the offer exists before deletion
+        return deleteOffer(offerToDelete);
     }
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

export const useDeleteOffer = () => useMutation({
    mutationFn: async (offerToDelete: Offer) => {
        if (!offerToDelete.id) {
            throw new Error('Cannot delete offer: ID is required');
        }
        // Validate that the offer exists before deletion
        return deleteOffer(offerToDelete);
    }
});

10-20: 🛠️ Refactor suggestion

Consider enhancing error handling and type safety.

While the basic functionality is correct, there are several improvements that could make this hook more robust:

 export const useUpdateOffer = () => useMutation({
     mutationFn: async (useOffers: UseOffers) => {
         if (useOffers.id) {
-            const headers = {
+            const headers: Record<string, string> = {
                 "Content-Type": "multipart/form-data",
             };
+            if (useOffers.file) {
+                const maxSize = 5 * 1024 * 1024; // 5MB
+                if (useOffers.file.size > maxSize) {
+                    throw new Error('File size exceeds 5MB limit');
+                }
+                if (!useOffers.file.type.startsWith('image/')) {
+                    throw new Error('Only image files are allowed');
+                }
+            }
             return await putOffer(useOffers.id, useOffers.offer, useOffers.file, headers);
         }
-        throw new Error('ID is missing');
+        throw new Error('Offer ID is required for updates');
     }
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

export const useUpdateOffer = () => useMutation({
    mutationFn: async (useOffers: UseOffers) => {
        if (useOffers.id) {
            const headers: Record<string, string> = {
                "Content-Type": "multipart/form-data",
            };
            if (useOffers.file) {
                const maxSize = 5 * 1024 * 1024; // 5MB
                if (useOffers.file.size > maxSize) {
                    throw new Error('File size exceeds 5MB limit');
                }
                if (!useOffers.file.type.startsWith('image/')) {
                    throw new Error('Only image files are allowed');
                }
            }
            return await putOffer(useOffers.id, useOffers.offer, useOffers.file, headers);
        }
        throw new Error('Offer ID is required for updates');
    }
});

32-41: 🛠️ Refactor suggestion

Add type safety and consistent error handling.

The creation hook needs better type safety and should maintain consistency with other hooks' error handling patterns.

 export const useCreateNewOffer = () =>
     useMutation({
-        mutationFn: async (newOffers: UseOffers) => {
+        mutationFn: async (newOffers: UseOffers): Promise<void> => {
+            if (!newOffers.offer.title || !newOffers.offer.description) {
+                throw new Error('Title and description are required');
+            }
             const headers = {
                 "Content-Type": "multipart/form-data",
             };
 
+            if (newOffers.file) {
+                const maxSize = 5 * 1024 * 1024; // 5MB
+                if (newOffers.file.size > maxSize) {
+                    throw new Error('File size exceeds 5MB limit');
+                }
+                if (!newOffers.file.type.startsWith('image/')) {
+                    throw new Error('Only image files are allowed');
+                }
+            }
             await postOffer(newOffers.offer, newOffers.file, headers);
         },
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

export const useCreateNewOffer = () =>
    useMutation({
        mutationFn: async (newOffers: UseOffers): Promise<void> => {
            if (!newOffers.offer.title || !newOffers.offer.description) {
                throw new Error('Title and description are required');
            }
            const headers = {
                "Content-Type": "multipart/form-data",
            };

            if (newOffers.file) {
                const maxSize = 5 * 1024 * 1024; // 5MB
                if (newOffers.file.size > maxSize) {
                    throw new Error('File size exceeds 5MB limit');
                }
                if (!newOffers.file.type.startsWith('image/')) {
                    throw new Error('Only image files are allowed');
                }
            }
            await postOffer(newOffers.offer, newOffers.file, headers);
        },
    });
frontend/src/features/admin/features/the-offers/api/OffersManipulationClient.ts (3)

56-62: ⚠️ Potential issue

Improve error handling and null checks

The function has several issues:

  1. Redundant null check || offer.imageLink != undefined
  2. Missing error handling for S3 operations
  3. Risk of orphaned S3 files if offer deletion fails

Consider this improved implementation:

 export const deleteOffer = async (offer: Offer) => {
-    if(offer.imageLink != null || offer.imageLink != undefined){
-    const deleteOldFile = `/s3/delete?link=${encodeURIComponent(offer.imageLink.toString())}`;
-    await httpDeleteS3File(deleteOldFile);
-    }
-    return httpDeleteJson("/offers/" + offer.id);
+    try {
+        if (offer.imageLink) {
+            const deleteOldFile = `/s3/delete?link=${encodeURIComponent(offer.imageLink)}`;
+            await httpDeleteS3File(deleteOldFile);
+        }
+        return await httpDeleteJson("/offers/" + offer.id);
+    } catch (error) {
+        // If offer deletion fails after successful image deletion,
+        // log the orphaned image for cleanup
+        console.error(`Failed to delete offer: ${error.message}`);
+        if (error.message.includes('image deleted but offer deletion failed')) {
+            console.error(`Orphaned S3 image: ${offer.imageLink}`);
+        }
+        throw error;
+    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

export const deleteOffer = async (offer: Offer) => {
    try {
        if (offer.imageLink) {
            const deleteOldFile = `/s3/delete?link=${encodeURIComponent(offer.imageLink)}`;
            await httpDeleteS3File(deleteOldFile);
        }
        return await httpDeleteJson("/offers/" + offer.id);
    } catch (error) {
        // If offer deletion fails after successful image deletion,
        // log the orphaned image for cleanup
        console.error(`Failed to delete offer: ${error.message}`);
        if (error.message.includes('image deleted but offer deletion failed')) {
            console.error(`Orphaned S3 image: ${offer.imageLink}`);
        }
        throw error;
    }
}

5-25: ⚠️ Potential issue

Add error handling and validation for file upload

The function lacks several important validations and error handling:

  1. File size and type validation before upload
  2. Error handling for S3 upload failures
  3. Type validation for the imageLink response

Consider implementing these improvements:

 export const postOffer = async (offer: Offer, file?: File, headers?: Record<string, string>) => {
+    const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
+    const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif'];
 
     if (file) {
+        if (file.size > MAX_FILE_SIZE) {
+            throw new Error('File size exceeds 5MB limit');
+        }
+        if (!ALLOWED_TYPES.includes(file.type)) {
+            throw new Error('Invalid file type. Allowed types: JPG, PNG, GIF');
+        }
         const formData = new FormData();
         formData.append("file", file);
 
-        const linkResponse = await httpPostMultipart<string>(
-            `/s3/upload`,
-            formData,
-            headers
-        );
+        try {
+            const linkResponse = await httpPostMultipart<string>(
+                `/s3/upload`,
+                formData,
+                headers
+            );
+            if (!linkResponse || typeof linkResponse !== 'string') {
+                throw new Error('Invalid response from S3 upload');
+            }
+            offer.imageLink = linkResponse;
+        } catch (error) {
+            throw new Error(`Failed to upload image: ${error.message}`);
+        }
-        offer.imageLink = linkResponse;
     }
 
     return httpPostJson<Offer>("/offers", offer);
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

export const postOffer = async (offer: Offer, file?: File, headers?: Record<string, string>) => {
    const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
    const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif'];

    if (file) {
        if (file.size > MAX_FILE_SIZE) {
            throw new Error('File size exceeds 5MB limit');
        }
        if (!ALLOWED_TYPES.includes(file.type)) {
            throw new Error('Invalid file type. Allowed types: JPG, PNG, GIF');
        }
        const formData = new FormData();
        formData.append("file", file);

        try {
            const linkResponse = await httpPostMultipart<string>(
                `/s3/upload`,
                formData,
                headers
            );
            if (!linkResponse || typeof linkResponse !== 'string') {
                throw new Error('Invalid response from S3 upload');
            }
            offer.imageLink = linkResponse;
        } catch (error) {
            throw new Error(`Failed to upload image: ${error.message}`);
        }
    }

    return httpPostJson<Offer>("/offers", offer);
};

28-54: 🛠️ Refactor suggestion

Refactor for better error handling and code reuse

The function has several areas for improvement:

  1. Duplicate file validation logic with postOffer
  2. Inadequate error handling for S3 operations
  3. Inconsistent null checks

Consider extracting shared logic and improving error handling:

+const validateAndUploadFile = async (file: File, headers?: Record<string, string>) => {
+    const MAX_FILE_SIZE = 5 * 1024 * 1024;
+    const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif'];
+
+    if (file.size > MAX_FILE_SIZE) {
+        throw new Error('File size exceeds 5MB limit');
+    }
+    if (!ALLOWED_TYPES.includes(file.type)) {
+        throw new Error('Invalid file type. Allowed types: JPG, PNG, GIF');
+    }
+
+    const formData = new FormData();
+    formData.append("file", file);
+    return httpPostMultipart<string>(`/s3/upload`, formData, headers);
+};

 export const putOffer = async (id: string, offer: Offer, file?: File, headers?: Record<string, string>) => {
     if (file) {
-        const formData = new FormData();
-        formData.append("file", file);
-
-        if(offer.imageLink != null){
-            formData.append("link", offer.imageLink.toString())
-
-            const deleteOldFile = `/s3/delete?link=${encodeURIComponent(offer.imageLink.toString())}`;
-            await httpDeleteS3File(deleteOldFile);
-        }
-
-        const newFileLink = await httpPostMultipart<string>(
-            `/s3/upload`,
-            formData,
-            headers
-        );
-
-        offer.imageLink = newFileLink;
+        try {
+            if (offer.imageLink) {
+                const deleteOldFile = `/s3/delete?link=${encodeURIComponent(offer.imageLink)}`;
+                await httpDeleteS3File(deleteOldFile);
+            }
+            
+            const newFileLink = await validateAndUploadFile(file, headers);
+            if (!newFileLink || typeof newFileLink !== 'string') {
+                throw new Error('Invalid response from S3 upload');
+            }
+            offer.imageLink = newFileLink;
+        } catch (error) {
+            throw new Error(`Failed to update image: ${error.message}`);
+        }
     }
     
     return httpPutJson<Offer>("/offers/" + id, offer);
 }

Committable suggestion skipped: line range outside the PR's diff.

frontend/src/features/admin/features/the-offers/i18n.ts (1)

28-31: ⚠️ Potential issue

Remove duplicate Map entries.

The following entries are duplicated and will cause the second entry to override the first:

  • ADD_LINK on lines 28 and 29
  • ADD_FILE on lines 30 and 31

Apply this diff to remove the duplicates:

    [AdminOffersLabels.ADD_CONTACT, `${i18n.t(`${ADMIN_OFFERS_PATH}.${AdminOffersLabels.ADD_CONTACT}`)}`],
    [AdminOffersLabels.ADD_LINK, `${i18n.t(`${ADMIN_OFFERS_PATH}.${AdminOffersLabels.ADD_LINK}`)}`],
-   [AdminOffersLabels.ADD_LINK, `${i18n.t(`${ADMIN_OFFERS_PATH}.${AdminOffersLabels.ADD_LINK}`)}`],
    [AdminOffersLabels.ADD_FILE, `${i18n.t(`${ADMIN_OFFERS_PATH}.${AdminOffersLabels.ADD_FILE}`)}`],
-   [AdminOffersLabels.ADD_FILE, `${i18n.t(`${ADMIN_OFFERS_PATH}.${AdminOffersLabels.ADD_FILE}`)}`],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    [AdminOffersLabels.ADD_LINK, `${i18n.t(`${ADMIN_OFFERS_PATH}.${AdminOffersLabels.ADD_LINK}`)}`],
    [AdminOffersLabels.ADD_FILE, `${i18n.t(`${ADMIN_OFFERS_PATH}.${AdminOffersLabels.ADD_FILE}`)}`],
frontend/src/features/the-footer-main/the-footer-main.vue (2)

2-8: ⚠️ Potential issue

Fix incorrect Vuetify class name

The class justify-right is not a standard Vuetify class. Use justify-end instead to align content to the right.

-      class="align-center justify-right"
+      class="align-center justify-end"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

  <v-container>
    <v-footer
      min-height="75px"
      padless
      color="transparent"
      class="align-center justify-end"
    >

20-28: ⚠️ Potential issue

Remove unused tooltip directives

The button has v-bind="attrs" and v-on="on" directives but no v-tooltip wrapper. These directives won't have any effect without the tooltip context.

              <v-btn
                class="mx-2 pt-0 mt-0"
                color="secondary"
-                v-bind="attrs"
                @click="openInfoDialog"
-                v-on="on"
              >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

              <v-btn
                class="mx-2 pt-0 mt-0"
                color="secondary"
                @click="openInfoDialog"
              >
                <v-icon>mdi-information-outline</v-icon> {{ infoLabels.informationTitle }}
              </v-btn>
frontend/src/features/commons/base-link-card/base-link-card.vue (1)

99-104: 🛠️ Refactor suggestion

Consider refactoring the CSS class implementation.

The current implementation has several potential issues:

  1. Using !important flags should be avoided as they make styles harder to maintain
  2. The class name "title" might conflict with Vuetify's built-in classes
  3. The text-indent: 0em property with !important suggests fighting against inherited styles

Consider this alternative implementation:

-.title{
-  white-space: pre-wrap !important;
-  text-indent: 0em !important;
-  word-break: keep-all;
-  line-height: normal;
-}
+.base-link-card-title {
+  white-space: pre-wrap;
+  word-break: keep-all;
+  line-height: normal;
+}

Then update the template:

-<v-card-title class="title">
+<v-card-title class="base-link-card-title">

This approach:

  • Uses a more specific class name to avoid conflicts
  • Removes unnecessary !important flags
  • Removes redundant text-indent property

Committable suggestion skipped: line range outside the PR's diff.

frontend/src/features/admin/features/the-offers/components/SaveNewButton.vue (3)

74-82: 🛠️ Refactor suggestion

Enhance error handling

The error handling could be improved:

  1. Reset loading state on error
  2. Add type safety for error object
  3. Consider using a dedicated error handling utility
-catch((error) => {
+catch((error: unknown) => {
+  isLoading.value = false;
   const fallbackErrorMessage = "An unexpected error occurred";
-  const customErrorMessage = error.response?.data?.message || fallbackErrorMessage;
+  const customErrorMessage = error instanceof Error 
+    ? error.message 
+    : fallbackErrorMessage;
   errorMessage.value = customErrorMessage;
   isWriteError.value = true;
   emit("error", customErrorMessage);
 });

Committable suggestion skipped: line range outside the PR's diff.


34-44: 🛠️ Refactor suggestion

Enhance prop definitions with validation

The props could benefit from additional validation and type safety:

  1. offerToSave should be marked as required
  2. file type should be more specific with File | null
  3. Add validation for offerToSave properties
 offerToSave: {
   type: Object as () => Offer,
+  required: true,
+  validator: (value: Offer) => {
+    return value && typeof value.title === 'string'
+  }
 },
 disabled: {
   type: Boolean,
   default: false
 },
 file: {
-  type: File,
+  type: [File, null] as PropType<File | null>,
   default: null
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    offerToSave: {
      type: Object as () => Offer,
      required: true,
      validator: (value: Offer) => {
        return value && typeof value.title === 'string'
      }
    },
    disabled: {
      type: Boolean,
      default: false
    },
    file: {
      type: [File, null] as PropType<File | null>,
      default: null
    }

68-71: ⚠️ Potential issue

Improve navigation handling

The current implementation has several issues:

  1. Using router.go(0) forces a page reload which is an anti-pattern in SPAs
  2. Hardcoded route path reduces maintainability
  3. Potential race condition with setTimeout if multiple saves are attempted
-setTimeout(() => {
-  router.push("/admin/angebote/");
-  router.go(0);
-}, 1000); // delay for 1 second
+const ROUTES = {
+  OFFERS_LIST: '/admin/angebote/'
+};
+await router.push(ROUTES.OFFERS_LIST);

Committable suggestion skipped: line range outside the PR's diff.

frontend/src/features/the-offers/the-offer.vue (6)

27-27: ⚠️ Potential issue

Security Warning: Unsafe HTML rendering

Using v-html to render user-provided content can expose the application to XSS attacks. Consider using a sanitization library like DOMPurify.

-<span v-html="offer.description" />
+<span v-html="sanitizeHtml(offer.description)" />

Add the following import and sanitization:

import DOMPurify from 'dompurify';

// In setup():
const sanitizeHtml = (html: string) => DOMPurify.sanitize(html);

72-72: ⚠️ Potential issue

Handle error state in the UI

The error state from useGetOffer is destructured but never used in the template. Consider displaying error messages to users.

-const { isLoading, data: offer, error } = useGetOffer(offerId);
+const { isLoading, data: offer, error } = useGetOffer(offerId);
+
+const showError = computed(() => {
+  if (error.value) {
+    return `Failed to load offer: ${error.value.message}`;
+  }
+  return null;
+});

And in template:

<v-alert
  v-if="showError"
  type="error"
  class="mb-4"
>
  {{ showError }}
</v-alert>

71-72: 🛠️ Refactor suggestion

Add type safety for offer data

Define an interface for the offer structure to ensure type safety throughout the component.

interface Offer {
  id: string;
  title: string;
  description: string;
  startDate?: string;
  endDate?: string;
  imageLink: string;
}

// Then use it:
const offer = ref<Offer | null>(null);

39-44: ⚠️ Potential issue

Add alt text for accessibility

Images should have descriptive alt text for screen readers and fallback scenarios.

 <img
   :src="offer.imageLink"
+  :alt="offer.title || 'Offer image'"
   class="cropped-image"
   contain
 >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

                  <img
                    :src="offer.imageLink"
                    :alt="offer.title || 'Offer image'"
                    class="cropped-image"
                    contain
                  >
                </div>

74-76: 🛠️ Refactor suggestion

Avoid hard-coded routes

Using hard-coded route strings can lead to maintenance issues. Consider using route names or constants.

-const back = () => {
-    router.push("/angebot");
-};
+const ROUTES = {
+  OFFERS_LIST: '/angebot'
+} as const;
+
+const back = () => {
+    router.push(ROUTES.OFFERS_LIST);
+};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        const ROUTES = {
          OFFERS_LIST: '/angebot'
        } as const;

        const back = () => {
            router.push(ROUTES.OFFERS_LIST);
        };

90-92: 🛠️ Refactor suggestion

Add missing image styles

The template uses image-container, image-wrapper, and cropped-image classes, but they're not defined in the style section.

 <style scoped>
-
+.image-container {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.image-wrapper {
+  width: 100%;
+  max-width: 500px;
+  aspect-ratio: 16/9;
+  overflow: hidden;
+}
+
+.cropped-image {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
 </style>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

<style scoped>
.image-container {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
}

.image-wrapper {
  width: 100%;
  max-width: 500px;
  aspect-ratio: 16/9;
  overflow: hidden;
}

.cropped-image {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
</style>
frontend/src/features/admin/features/the-offers/components/DeleteButton.vue (2)

83-97: 🛠️ Refactor suggestion

Improve error handling and navigation flow

The current implementation has several potential issues:

  1. The delete event is emitted before the actual deletion occurs
  2. Using router.go(0) forces a full page refresh, which is not ideal for SPA
  3. Error handling doesn't provide user feedback

Consider this improved implementation:

     const deleteOffer = () => {
       if (props.currentItem) {
-        emit("delete", props.currentItem);
-      }
-      isDialogActive.value = false;
-      mutateAsync(props.currentItem)
-        .then(() => {
-          isSnackbarActive.value = true;
-          setTimeout(() => {
-            router.push("/admin/angebote/");
-            router.go(0);
-          }, 1000); // delay for 1 second
-        })
-        .catch(() => emit("error"));
+        isDialogActive.value = false;
+        mutateAsync(props.currentItem)
+          .then(() => {
+            emit("delete", props.currentItem);
+            isSnackbarActive.value = true;
+            setTimeout(() => {
+              router.push("/admin/angebote/");
+            }, REDIRECT_DELAY);
+          })
+          .catch((error) => {
+            emit("error", error);
+            isSnackbarActive.value = true;
+            snackbarMessage.value = "Fehler beim Löschen des Angebots";
+          });
+      }
     };

Committable suggestion skipped: line range outside the PR's diff.


21-24: ⚠️ Potential issue

Fix grammatical errors in the German text

The dialog text contains grammatical errors:

  • "das Angebote" should be "das Angebot" (singular)
  • "der Angebote" should be "des Angebots" (singular)
-          <v-card>Soll das Angebote wirklich gelöscht werden?</v-card>
+          <v-card>Soll das Angebot wirklich gelöscht werden?</v-card>
           <v-card-text>
-            Das Löschen der Angebote kann nicht rückgängig gemacht werden. Eine Löschung kann nur durch das
-            erneute Anlegen der Angebote bereinigt werden.
+            Das Löschen des Angebots kann nicht rückgängig gemacht werden. Eine Löschung kann nur durch das
+            erneute Anlegen des Angebots bereinigt werden.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

          <v-card>Soll das Angebot wirklich gelöscht werden?</v-card>
          <v-card-text>
            Das Löschen des Angebots kann nicht rückgängig gemacht werden. Eine Löschung kann nur durch das
            erneute Anlegen des Angebots bereinigt werden.
frontend/src/features/admin/features/the-offers/components/SaveUpdateButton.vue (3)

81-84: ⚠️ Potential issue

Remove anti-pattern page reload and handle navigation properly.

Using router.go(0) forces a page reload which is an anti-pattern in Vue applications. This can cause unnecessary server requests and a poor user experience.

- setTimeout(() => {
-   router.push("/admin/angebote/" + editedOffer.value?.id);
-   router.go(0);
- }, 1000); // delay for 1 second
+ await router.push("/admin/angebote/" + editedOffer.value?.id);

Committable suggestion skipped: line range outside the PR's diff.


33-48: 🛠️ Refactor suggestion

Strengthen props validation and documentation.

The props need stricter validation and documentation for better maintainability and type safety.

 props: {
   offerToSave: {
     type: Object as () => Offer,
+    required: true,
+    validator: (value: Offer) => {
+      return value !== null && typeof value === 'object';
+    }
   },
   id: {
     type: String,
+    required: true
   },
   disabled: {
     type: Boolean,
     default: false,
+    validator: (value: boolean) => typeof value === 'boolean'
   },
   file: {
     type: File,
     default: null,
+    validator: (value: File | null) => value === null || value instanceof File
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

  props: {
    offerToSave: {
      type: Object as () => Offer,
      required: true,
      validator: (value: Offer) => {
        return value !== null && typeof value === 'object';
      }
    },
    id: {
      type: String,
      required: true
    },
    disabled: {
      type: Boolean,
      default: false,
      validator: (value: boolean) => typeof value === 'boolean'
    },
    file: {
      type: File,
      default: null,
      validator: (value: File | null) => value === null || value instanceof File
    }
  },

61-100: 🛠️ Refactor suggestion

Refactor saveAdd function for better error handling and cleanup.

The function has several issues including potential memory leaks and unnecessary object copying.

+ const timeoutRef = ref<number | null>(null);
+ 
+ onBeforeUnmount(() => {
+   if (timeoutRef.value) {
+     clearTimeout(timeoutRef.value);
+   }
+ });

 function saveAdd(file?: File | null) {
-  const headers = {
-    'Content-Type': 'multipart/form-data'
-  };

   if (!props.offerToSave?.imageLink || props.offerToSave.imageLink.length === 0) {
     emit("error", "Bild ist erforderlich.");
     return;
   }

-  editedOffer.value = { ...props.offerToSave }; // Create a copy of offerToSave
-  if (editedOffer.value) {
+  if (props.offerToSave) {
     mutateAsync({
-      offer: editedOffer.value,
+      offer: props.offerToSave,
       id: props.offerToSave.id,
       file: file ? file : undefined,
-      image: editedOffer.value.imageLink,
+      image: props.offerToSave.imageLink,
     })
     .then(() => {
       showSuccessSnackbar();
-      setTimeout(() => {
-        router.push("/admin/angebote/" + editedOffer.value?.id);
-        router.go(0);
-      }, 1000);
+      timeoutRef.value = window.setTimeout(async () => {
+        await router.push("/admin/angebote/" + props.offerToSave.id);
+      }, 1000);
     })
     .catch((error) => {
       const fallbackErrorMessage = "An unexpected error occurred";
       const customErrorMessage = error.response?.data?.message || fallbackErrorMessage;
       errorMessage.value = customErrorMessage;
       isWriteError.value = true;
-
-      // Emit the error event to the parent component
       emit("error", customErrorMessage);
     });
   } else {
-    // handle case where editedOffer.value is null
     emit("error", "No offer is available for editing.");
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    const timeoutRef = ref<number | null>(null);

    onBeforeUnmount(() => {
      if (timeoutRef.value) {
        clearTimeout(timeoutRef.value);
      }
    });

    function saveAdd(file?: File | null) {
      if (!props.offerToSave?.imageLink || props.offerToSave.imageLink.length === 0) {
        emit("error", "Bild ist erforderlich.");
        return;
      }

      if (props.offerToSave) {
        mutateAsync({
          offer: props.offerToSave,
          id: props.offerToSave.id,
          file: file ? file : undefined,
          image: props.offerToSave.imageLink,
        })
          .then(() => {
            showSuccessSnackbar();
            timeoutRef.value = window.setTimeout(async () => {
              await router.push("/admin/angebote/" + props.offerToSave.id);
            }, 1000);
          })
          .catch((error) => {
            const fallbackErrorMessage = "An unexpected error occurred";
            const customErrorMessage = error.response?.data?.message || fallbackErrorMessage;
            errorMessage.value = customErrorMessage;
            isWriteError.value = true;
            emit("error", customErrorMessage);
          });
      } else {
        emit("error", "No offer is available for editing.");
      }
    }
frontend/src/features/admin/features/the-additional/features/the-faq/the-faq.vue (1)

54-57: ⚠️ Potential issue

Remove duplicate BackButton component.

The BackButton component appears twice in the template (lines 3 and 54-57) with the same callback function. This violates the DRY principle and might confuse users. Consider keeping only one instance, preferably at the top of the page.

- <BackButton
-   :callback="back"
-   class="mt-1"
- />

Committable suggestion skipped: line range outside the PR's diff.

frontend/src/features/admin/features/the-additional/features/the-downloads/the-downloads.vue (2)

54-57: ⚠️ Potential issue

Remove duplicate BackButton

There are two identical BackButtons with the same callback. This is redundant and could be confusing for users. Consider keeping only one, preferably at the top of the page for consistency with other admin pages.

-<BackButton
-  :callback="back"
-  class="mt-1"
->

47-47: ⚠️ Potential issue

Fix typo in prop name filteredDownloadsy

The prop name appears to have an unintended 'y' at the end. This should be renamed to filteredDownloads for clarity and consistency.

-:items="filteredDownloadsy"
+:items="filteredDownloads"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

                :items="filteredDownloads"
frontend/src/features/admin/features/the-additional/features/the-glossar/the-glossar.vue (2)

85-88: ⚠️ Potential issue

Remove duplicate BackButton component

There are two instances of the BackButton component in the template (one at the top and one at the bottom) with the same callback. This duplication could be confusing for users and maintenance.

Consider keeping only one instance, preferably at the top of the page following common navigation patterns.


51-71: 🛠️ Refactor suggestion

Enhance accessibility and simplify text alignment

A few suggestions to improve this section:

  1. The text alignment classes can be simplified
  2. Buttons should have type attributes for better semantics
  3. Consider using a more semantic separator

Consider applying these improvements:

-              <p class="text-xs-center text-sm-center text-md-center text-lg-center text-xl-center">
+              <p class="text-center">
               <button
+                type="button"
                 :class="{ active: filterLetter === '' }"
                 class="pa-2"
                 @click="filterLetter = ''"
               >
                 Filter Zurücksetzen
               </button>
               <span
                 v-for="(letter, letterIndex) in glossaryAlphabet"
                 :key="letterIndex"
               ><button
+                type="button"
                 :key="letterIndex"
                 :class="{ active: filterLetter === letter }"
                 class="pa-2"
                 @click="filterLetter = letter"
               >{{
                 letter
-              }}</button><span v-if="letterIndex + 1 !== glossaryAlphabet.length">·</span></span>
+              }}</button><span v-if="letterIndex + 1 !== glossaryAlphabet.length" aria-hidden="true" class="mx-1">·</span></span>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

              <p class="text-center">
                <button
                  type="button"
                  :class="{ active: filterLetter === '' }"
                  class="pa-2"
                  @click="filterLetter = ''"
                >
                  Filter Zurücksetzen
                </button>

                <span
                  v-for="(letter, letterIndex) in glossaryAlphabet"
                  :key="letterIndex"
                ><button
                  type="button"
                  :key="letterIndex"
                  :class="{ active: filterLetter === letter }"
                  class="pa-2"
                  @click="filterLetter = letter"
                >{{
                  letter
                }}</button><span v-if="letterIndex + 1 !== glossaryAlphabet.length" aria-hidden="true" class="mx-1">·</span></span>
              </p>
frontend/src/core/translations/de-DE.json (3)

87-87: ⚠️ Potential issue

Fix grammatical error in content items description

The article doesn't match the gender of "Artikel" (masculine).

Apply this correction:

-        "addNewDescription": "Fügen Sie eine neuen Artikel hinzu.",
+        "addNewDescription": "Fügen Sie einen neuen Artikel hinzu.",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        "addNewDescription": "Fügen Sie einen neuen Artikel hinzu.",

69-83: 💡 Codebase verification

Fix grammatical inconsistencies in German translations

Several grammatical issues found in the translations:

  • "addNewDescription" in offers section: "Fügen Sie eine neues Angebot hinzu" has incorrect article agreement (should be "ein neues")
  • "edit" in offers section refers to "Anlaufstelle" but is used in the offers context
  • "addNewDescription" in contentItems: "Fügen Sie eine neuen Artikel" has incorrect article agreement (should be "einen neuen")

The translations should be:

  • offers.texts.addNewDescription: "Fügen Sie ein neues Angebot hinzu."
  • offers.texts.edit: "Bearbeiten Sie ein bestehendes Angebot."
  • contentItems.texts.addNewDescription: "Fügen Sie einen neuen Artikel hinzu."
🔗 Analysis chain

Verify translation consistency across similar sections

The file has multiple sections (contactPoints, offers, contentItems) with similar translation keys but different contexts. Let's verify the consistency of translations across these sections.

Also applies to: 87-87

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Compare translation keys and their values across similar sections

# Extract and compare the structure of similar sections
jq -r '.Admin | {contactPoints,offers,contentItems}' frontend/src/core/translations/de-DE.json

# List all occurrences of potentially inconsistent terms
echo "Checking for 'Anlaufstelle'..."
rg -i "anlaufstelle" frontend/src/core/translations/

echo "Checking for article agreement..."
rg -i "fügen sie eine" frontend/src/core/translations/

Length of output: 4348


69-83: ⚠️ Potential issue

Fix grammatical errors and incorrect terminology in offers section

There are several issues in the German translations:

  1. Grammar: "eine neues" should be "ein neues" (line 72)
  2. Context: "Anlaufstelle" should be "Angebot" in the edit message (line 77)

Apply these corrections:

  "offers": {
    "texts": {
      "addNewTitle": "Hinzufügen",
-     "addNewDescription": "Fügen Sie eine neues Angebot hinzu.",
+     "addNewDescription": "Fügen Sie ein neues Angebot hinzu.",
      "addDepartment": "Referat",
      "addContact": "Kontakt hinzufügen",
      "addLink": "Link hinzufügen",
      "addFile": "Datei hinzufügen",
-     "edit": "Bearbeiten Sie eine bestehende Anlaufstelle.",
+     "edit": "Bearbeiten Sie ein bestehendes Angebot.",
      "cancel": "Abbrechen",
      "save": "Speichern",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    "offers": {
      "texts": {
        "addNewTitle": "Hinzufügen",
        "addNewDescription": "Fügen Sie ein neues Angebot hinzu.",
        "addDepartment": "Referat",
        "addContact": "Kontakt hinzufügen",
        "addLink": "Link hinzufügen",
        "addFile": "Datei hinzufügen",
        "edit": "Bearbeiten Sie ein bestehendes Angebot.",
        "cancel": "Abbrechen",
        "save": "Speichern",
        "markdownHint": "Die Beschreibung kann mittels Markdown (Empfehlung) oder HTML designed werden. Das Ergebnis sehen Sie in dem Feld daneben. Über diesen Link können Sie sich die verschiedenen Möglichkeiten von Markdown ansehen: ",
        "addDepartmentHint":"Um ein weiteres Referat hinzuzufügen geben Sie dies ein und drücken Sie bitte <kbd>enter</kbd>."
      }
    },
frontend/src/features/admin/features/the-contact-points/the-contact-points-overview.vue (2)

26-52: 🛠️ Refactor suggestion

Simplify list structure and enhance accessibility

The current implementation has some structural and accessibility concerns:

  1. There's unnecessary nesting of v-list components which adds complexity
  2. The list structure could benefit from proper ARIA attributes for better accessibility

Consider applying these changes:

-          <v-list>
            <NewContactPointListItem
              :label="label"
              :set-is-add-new="setIsAddNew"
              :disabled="selectedItem !== undefined || isAddNew"
            />
-          </v-list>
           <v-list v-if="listItems && listItems.length > 0">
             <v-list-item
               v-for="item in listItems"
               :key="item.id"
               three-line
               link
               :disabled="selectedItem !== undefined || isAddNew"
+              :aria-selected="item === selectedItem"
+              role="option"
               :class="{ 'selected': item === selectedItem }"
               @click="setSelectedItem(item)"
             >

Committable suggestion skipped: line range outside the PR's diff.


107-108: 💡 Codebase verification

Remove unused import useGetAdminUserInfo

The useGetAdminUserInfo hook is imported but not used anywhere in the component. It should be removed to keep the codebase clean.

🔗 Analysis chain

Verify usage of imported admin user info hook

The useGetAdminUserInfo hook is imported but doesn't appear to be used in the component.

Let's verify if this import is needed:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if useGetAdminUserInfo is used in the component
ast-grep --pattern 'useGetAdminUserInfo' frontend/src/features/admin/features/the-contact-points/the-contact-points-overview.vue

# Check if it's used in related components
rg -l 'useGetAdminUserInfo' frontend/src/features/admin/

Length of output: 678


Script:

#!/bin/bash
# Check the actual usage of useGetAdminUserInfo in the component
ast-grep --pattern 'useGetAdminUserInfo($$$)' frontend/src/features/admin/features/the-contact-points/the-contact-points-overview.vue

# Get more context around any potential usage
rg -C 3 'useGetAdminUserInfo' frontend/src/features/admin/features/the-contact-points/the-contact-points-overview.vue

Length of output: 774

frontend/src/features/the-offers/the-offers.vue (5)

154-154: ⚠️ Potential issue

Handle error state in the template.

The component retrieves error state from useGetOfferListItems but doesn't display error messages to users.

-<div v-else>
+<div v-else-if="isError">
+  {{ error?.message || 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.' }}
+</div>
+<div v-else>
   Zurzeit sind keine Angebote verfügbar. Schauen Sie später wieder vorbei.
 </div>

Also applies to: 123-125


109-109: ⚠️ Potential issue

Fix invalid style attribute syntax.

The style attribute has invalid syntax: style="font-size:14;". The value is missing a unit.

-<v-card-title style="font-size:14;">
+<v-card-title style="font-size: 14px;">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

                    <v-card-title style="font-size: 14px;">

45-45: ⚠️ Potential issue

Fix invalid style syntax.

The inline style has incorrect syntax: style="height:275px; !important". The semicolon before !important is invalid.

-style="height:275px; !important"
+style="height: 275px !important"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

                    style="height: 275px !important"

49-54: ⚠️ Potential issue

Add alt text for accessibility.

Images are missing alt text attributes, which is essential for accessibility. Add descriptive alt text for screen readers.

 <img
   :src="item.imageLink"
+  :alt="item.title"
   class="cropped-image"
   contain
 >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

                        <img
                          :src="item.imageLink"
                          :alt="item.title"
                          class="cropped-image"
                          contain
                        >
                      </div>

211-221: ⚠️ Potential issue

Fix transform translate percentages.

The transform translate percentages don't match the top/left positioning values.

 .image-fill {
   position: absolute;
   top: 25%;
   left: 25%;
-  transform: translate(-25%, -25%);
+  transform: translate(-50%, -50%);
   width: 100%;
   height: 100%;
   background-size: cover;
   background-repeat: no-repeat;
   background-position: center;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

.image-fill {
  position: absolute;
  top: 25%;
  left: 25%;
  transform: translate(-50%, -50%);
  width: 100%;
  height: 100%;
  background-size: cover;
  background-repeat: no-repeat;
  background-position: center;
}
frontend/src/features/admin/features/the-additional/features/the-leadership-cooperation/the-leadership-cooperation.vue (2)

157-160: 🛠️ Refactor suggestion

Remove duplicate BackButton component

There are two instances of BackButton with the same callback. The second instance (at the bottom) appears to be redundant as it serves the same purpose as the one at the top of the page.

-    <BackButton
-      :callback="back"
-      class="mt-1"
-    />

126-131: ⚠️ Potential issue

Security: Potential XSS vulnerability in markdown rendering

Using v-html with user-provided content could expose the application to XSS attacks. Consider using a secure markdown renderer with proper sanitization.

Consider using DOMPurify:

+ import DOMPurify from 'dompurify';
  // In computed property:
- v-html="computeMarkdown"
+ v-html="DOMPurify.sanitize(computeMarkdown)"

Committable suggestion skipped: line range outside the PR's diff.

frontend/src/features/admin/features/the-additional/features/the-conflict-prevention/the-conflict-prevention.vue (3)

9-9: 🛠️ Refactor suggestion

Remove duplicate BackButton component

The BackButton component appears twice in the template with the same callback. This violates the DRY principle and could be confusing for users.

Keep only one instance of the BackButton, preferably at the top of the page (line 9).

Also applies to: 157-160


31-96: 🛠️ Refactor suggestion

⚠️ Potential issue

Refactor text editor implementation for better security and maintainability

Several concerns with the current implementation:

  1. Direct DOM manipulation via querySelector is anti-pattern in Vue
  2. Using HTML tags for formatting could lead to XSS vulnerabilities
  3. The formatting logic could be more maintainable

Consider these improvements:

  1. Use Vue refs instead of querySelector
  2. Use markdown syntax instead of HTML tags
  3. Extract formatting logic into a separate composable

Example refactor:

- const textarea = document.querySelector('#description-textarea') as HTMLTextAreaElement | null;
+ const textareaRef = ref<HTMLTextAreaElement | null>(null);
<v-textarea
  ref="textareaRef"
  ...
/>

Also, consider using a dedicated markdown editor component like vue-markdown-editor which handles these concerns securely.

Also applies to: 226-270


126-130: 💡 Codebase verification

⚠️ Potential issue

Markdown content requires XSS protection

The verification confirms that the application uses raw markdown parsing without any sanitization:

  • computeMarkdown directly uses marked.parse() without sanitization
  • No global marked configuration for sanitization is found
  • No DOMPurify or other sanitization utilities are present
  • Direct use of v-html with unsanitized content poses XSS risks

Consider implementing one of these solutions:

  • Use DOMPurify to sanitize the markdown output
  • Configure marked with sanitize: true option
  • Use a Vue-specific markdown component that handles sanitization
🔗 Analysis chain

Verify markdown sanitization for XSS prevention

Using v-html with markdown content could expose the application to XSS attacks.

Consider using DOMPurify or configuring marked with proper sanitization:

import DOMPurify from 'dompurify';

const computeMarkdown = computed(() => 
  DOMPurify.sanitize(
    marked.parse(writableContentItem.value?.contentItemView?.[0]?.content || "")
  )
);
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if DOMPurify or similar sanitization is used
rg -l "DOMPurify|sanitize|xss" --type ts --type vue

# Check marked configuration
ast-grep --pattern 'marked.setOptions({ sanitize: true })'

Length of output: 142


Script:

#!/bin/bash
# Search for markdown-related imports and configurations
rg -g "*.{js,ts,vue}" "import.*marked|marked.setOptions|DOMPurify" -A 3

# Search for computeMarkdown computed property implementation
ast-grep --pattern 'computed(() => {
  $$$
})'

# Search for sanitization-related code
rg -g "*.{js,ts,vue}" "sanitize|xss|purify" -i

Length of output: 4755


Script:

#!/bin/bash
# Let's check the specific implementation of computeMarkdown in the conflict prevention component
rg -g "*.vue" "computeMarkdown" -B 2 -A 5 "frontend/src/features/admin/features/the-additional/features/the-conflict-prevention/the-conflict-prevention.vue"

# Check if marked is configured anywhere in the codebase
rg -g "*.{js,ts,vue}" "marked\.(setup|setOptions|use)" -B 2 -A 2

# Look for any markdown-related utility or helper files
fd -g "*mark*.{js,ts,vue}"

Length of output: 1214


Script:

#!/bin/bash
# Let's check the content of the MarkDownAlert component for any sanitization logic
cat "frontend/src/features/admin/features/commons/MarkDownAlert.vue"

# Check if there's any global marked configuration in main files
rg -g "*.{js,ts}" "marked" "frontend/src/main.ts" "frontend/src/app.ts" "frontend/src/plugins/*"

# Look for any security-related utilities
fd -g "*security*.{js,ts}" -g "*sanitize*.{js,ts}"

Length of output: 985

frontend/src/features/admin/features/the-contact-points/components/EditContactPoint.vue (2)

447-462: 🛠️ Refactor suggestion

Enhance error handling

Consider improving error handling by:

  1. Using specific error types for different failure scenarios
  2. Adding retry mechanisms for transient failures
  3. Providing more detailed error messages

Example implementation:

type ContactPointError = {
  type: 'READ' | 'WRITE' | 'VALIDATION';
  message: string;
  retryable: boolean;
};

const error = ref<ContactPointError | null>(null);
const retryCount = ref(0);

const handleError = (type: ContactPointError['type'], message: string) => {
  error.value = {
    type,
    message,
    retryable: type === 'READ' || type === 'WRITE'
  };
};

const retry = async () => {
  if (error.value?.retryable && retryCount.value < 3) {
    retryCount.value++;
    // Retry the failed operation
  }
};

418-418: ⚠️ Potential issue

Remove incorrect console error import

The import of error from 'console' is problematic as it shadows the local error function defined in the component. This could lead to unexpected behavior.

- import { error } from "console";
frontend/src/features/admin/the-admin-overview.vue (1)

87-89: ⚠️ Potential issue

Avoid Mutating Imported Route Objects

Directly modifying the path property of imported route objects (adminContactPointsRoutes and adminOffersRoutes) can lead to unintended side effects elsewhere in the application, as these objects may be used in other components or contexts. It's a best practice to avoid mutating imported objects to maintain predictable behavior.

Consider cloning the route objects before modifying them:

87       get anlaufstellen(): unknown {
-88         adminContactPointsRoutes.path = "/admin/anlaufstellen/";
-89         return adminContactPointsRoutes;
+88         return { ...adminContactPointsRoutes, path: "/admin/anlaufstellen/" };
89       }

96       get angebote(): unknown {
-97         adminOffersRoutes.path = "/admin/angebote/";
-98         return adminOffersRoutes;
+97         return { ...adminOffersRoutes, path: "/admin/angebote/" };
98       }

Also applies to: 96-98

frontend/src/features/admin/features/the-offers/the-offers-overview.vue (2)

146-147: ⚠️ Potential issue

Fix the incorrect condition in handleIdChange

The condition in the if statement seems malformed and may lead to unexpected behavior.

if (!item && !item == undefined && item == null) {

This condition is confusing and redundant. A simplified and correct condition would be:

if (!item) {

Apply this diff to correct the condition:

- if (!item && !item == undefined && item == null) {
+ if (!item) {

261-277: 🛠️ Refactor suggestion

Update deprecated ::v-deep selectors in styles

The ::v-deep combinator is deprecated in Vue 3. The recommended syntax is now using the :deep() pseudo-class.

Apply this diff to update the styles:

-::v-deep textarea::-webkit-scrollbar {
+:deep(textarea)::-webkit-scrollbar {
   width: 20px;
 }

-::v-deep textarea::-webkit-scrollbar-track {
+:deep(textarea)::-webkit-scrollbar-track {
   background-color: transparent;
 }

-::v-deep textarea::-webkit-scrollbar-thumb {
+:deep(textarea)::-webkit-scrollbar-thumb {
   background-color: #d6dee1;
   border-radius: 20px;
   border: 6px solid transparent;
   background-clip: content-box;
 }

-::v-deep textarea::-webkit-scrollbar-thumb:hover {
+:deep(textarea)::-webkit-scrollbar-thumb:hover {
   background-color: #a8bbbf;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

:deep(textarea)::-webkit-scrollbar {
  width: 20px;
}

:deep(textarea)::-webkit-scrollbar-track {
  background-color: transparent;
}

:deep(textarea)::-webkit-scrollbar-thumb {
  background-color: #d6dee1;
  border-radius: 20px;
  border: 6px solid transparent;
  background-clip: content-box;
}

:deep(textarea)::-webkit-scrollbar-thumb:hover {
  background-color: #a8bbbf;
frontend/src/features/admin/features/the-offers/components/NewOffer.vue (5)

280-280: ⚠️ Potential issue

Remove unnecessary import of 'error' from 'console'

Importing error from "console" is unnecessary and may conflict with the locally defined error method. Please remove this import statement.

Apply this diff:

- import { error } from "console";

382-382: ⚠️ Potential issue

Sanitize user input to prevent XSS vulnerabilities

The computeMarkdown computed property renders user input without sanitization. This could lead to XSS attacks. Please sanitize the output using a library like DOMPurify.

Apply this diff:

+ import DOMPurify from 'dompurify';

- const computeMarkdown = computed(() => marked.parse(newOffer.value?.description || ""));
+ const computeMarkdown = computed(() => DOMPurify.sanitize(marked.parse(newOffer.value?.description || "")));

Committable suggestion skipped: line range outside the PR's diff.


453-462: 🛠️ Refactor suggestion

Avoid direct DOM manipulation; use refs instead

The changeDescription method directly accesses the DOM using document.querySelector, which is against Vue's best practices. Use Vue refs to interact with DOM elements reactively.

Apply this diff:

+ const descriptionTextarea = ref<HTMLTextAreaElement | null>(null);

const changeDescription = () => {
- const textarea = document.querySelector('#description-textarea') as HTMLTextAreaElement;
- const value = textarea.value;
+ const value = descriptionTextarea.value?.value || '';
  if (newOffer.value) {
    newOffer.value = {
      ...newOffer.value,
      description: value
    };
  }
};

In the template:

<v-textarea
  id="description-textarea"
+ ref="descriptionTextarea"
  color="secondary"

Committable suggestion skipped: line range outside the PR's diff.


315-324: ⚠️ Potential issue

Review the logic in the 'watch' function for 'newOffer'

The watch function on newOffer appears to be unnecessary and could lead to unintended behavior. Since newOffer is a ref, changes to newOffer.value are reactive. Consider removing or revising this watcher.

Apply this diff to remove the watcher:

- watch(newOffer, (newValue) => {
-   if (!newOffer.value) {
-     newOffer.value = newValue;
-     if (newValue) {
-       displayStartDate.value = newValue.startDate ? formatDateForDisplay(newValue.startDate) : '';
-       displayEndDate.value = newValue.endDate ? formatDateForDisplay(newValue.endDate) : '';
-     }
-   }
- });

465-521: ⚠️ Potential issue

Use Markdown syntax in 'applyFormatting' instead of HTML tags

The applyFormatting method inserts HTML tags into the description, but since you're using the marked parser, it's better to insert Markdown syntax. This ensures proper rendering and reduces XSS risks.

Apply this diff to update formatting:

switch (format) {
  case 'bold':
-   newText = `<b>${selectedText}</b>`;
+   newText = `**${selectedText}**`;
    break;
  case 'italic':
-   newText = `<i>${selectedText}</i>`;
+   newText = `*${selectedText}*`;
    break;
  case 'underline':
-   newText = `<u>${selectedText}</u>`;
+   newText = `<ins>${selectedText}</ins>`;
    break;
  case 'ordered-list':
-   newText = `\n<ol>\n<li>${selectedText}</li>\n<li></li>\n<li></li>\n</ol>\n`;
+   newText = `\n1. ${selectedText}\n2. \n3. \n`;
    break;
  case 'unordered-list':
-   newText = `\n<ul>\n<li>${selectedText}</li>\n<li></li>\n<li></li>\n</ul>\n`;
+   newText = `\n- ${selectedText}\n- \n- \n`;
    break;
  case 'line-break':
-   newText = '<br />';
+   newText = '  \n';
    break;
  case 'h1':
-   newText = `<h1>${selectedText}</h1>`;
+   newText = `# ${selectedText}`;
    break;
  case 'h2':
-   newText = `<h2>${selectedText}</h2>`;
+   newText = `## ${selectedText}`;
    break;
  case 'h3':
-   newText = `<h3>${selectedText}</h3>`;
+   newText = `### ${selectedText}`;
    break;
  case 'h4':
-   newText = `<h4>${selectedText}</h4>`;
+   newText = `#### ${selectedText}`;
    break;
  case 'h5':
-   newText = `<h5>${selectedText}</h5>`;
+   newText = `##### ${selectedText}`;
    break;
  default:
    newText = selectedText;
    break;
}

Additionally, update the computeMarkdown to sanitize the parsed Markdown as mentioned earlier.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    const applyFormatting = (format: string) => {
      const textarea = document.querySelector('#description-textarea') as HTMLTextAreaElement | null;
      if (!textarea) return;

      const start = textarea.selectionStart;
      const end = textarea.selectionEnd;
      const selectedText = textarea.value.substring(start, end);

      let newText = '';
      switch (format) {
        case 'bold':
          newText = `**${selectedText}**`;
          break;
        case 'italic':
          newText = `*${selectedText}*`;
          break;
        case 'underline':
          newText = `<ins>${selectedText}</ins>`;
          break;
        case 'ordered-list':
          newText = `\n1. ${selectedText}\n2. \n3. \n`;
          break;
        case 'unordered-list':
          newText = `\n- ${selectedText}\n- \n- \n`;
          break;
        case 'line-break':
          newText = '  \n';
          break;
        case 'h1':
          newText = `# ${selectedText}`;
          break;
        case 'h2':
          newText = `## ${selectedText}`;
          break;
        case 'h3':
          newText = `### ${selectedText}`;
          break;
        case 'h4':
          newText = `#### ${selectedText}`;
          break;
        case 'h5':
          newText = `##### ${selectedText}`;
          break;
        default:
          newText = selectedText;
          break;
      }

      const currentValue = newOffer.value?.description || '';
      const newValue =
        currentValue.substring(0, start) + newText + currentValue.substring(end);
      newOffer.value = {
        ...newOffer.value,
        description: newValue
      } as Offer;
    };
frontend/src/features/admin/features/the-offers/components/EditOffer.vue (5)

425-425: ⚠️ Potential issue

Sanitize markdown content to prevent XSS vulnerabilities

Rendering user-generated markdown without sanitization can expose your application to cross-site scripting (XSS) attacks. Since marked no longer includes built-in sanitization, it's important to sanitize the output before rendering.

Integrate a sanitizer like DOMPurify to sanitize the parsed markdown.

  1. Install DOMPurify:
npm install dompurify
  1. Import DOMPurify in your script:
+import DOMPurify from 'dompurify';
  1. Update computeMarkdown to sanitize the HTML:
const computeMarkdown = computed(() => {
  const rawHtml = marked.parse(writableOffer.value?.description || "");
+ const cleanHtml = DOMPurify.sanitize(rawHtml);
+ return cleanHtml;
- return rawHtml;
});

This ensures that any malicious scripts are removed before rendering the content.


310-311: ⚠️ Potential issue

Remove unnecessary import of error from "console"

The import of error from "console" is unnecessary and may cause confusion. The console object is globally available in JavaScript, so you can use console.error directly without importing it.

Apply this diff to remove the unnecessary import:

-import { error } from "console";

Ensure that any references to error in your code are correctly handled.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

import { VContainer, VCard, VCardTitle, VCardText, VForm, VRow, VCol, VTextField, VMenu, VDatePicker, VDivider, VBtn, VIcon, VTextarea, VFileInput, VCardActions } from "vuetify/lib";

498-507: 🛠️ Refactor suggestion

Avoid direct DOM manipulation in changeDescription function

Accessing the DOM directly using document.querySelector is discouraged in Vue.js. Instead, use a ref to reference the textarea and maintain reactivity.

Refactor your code to use a ref:

Template:

<v-textarea
  id="description-textarea"
+ ref="descriptionTextarea"
  v-model="writableOffer.description"
  ...
/>

Script:

+const descriptionTextarea = ref<HTMLTextAreaElement | null>(null);

const changeDescription = () => {
- const textarea = document.querySelector('#description-textarea') as HTMLTextAreaElement;
+ const textarea = descriptionTextarea.value;
  if (!textarea) return;
  const value = textarea.value;
  if (writableOffer.value) {
    writableOffer.value = {
      ...writableOffer.value,
      description: value
    };
  }
};

Make sure to include descriptionTextarea in your returned data:

return {
  ...,
+ descriptionTextarea,
  ...
}

Committable suggestion skipped: line range outside the PR's diff.


383-415: ⚠️ Potential issue

Correct validation rules in validateDateRange and validateDateFields

The computed properties validateDateRange and validateDateFields are not returning functions as required by Vuetify's validation rules. Instead, they return objects or booleans, which may cause the validation to not work as expected.

Refactor the validation computed properties to return functions that accept the value and return true or an error message.

Example refactor for validateDateRange:

-const validateDateRange = computed(() => {
-  const startDate = writableOffer.value?.startDate;
-  const endDate = writableOffer.value?.endDate;
-
-  if (startDate && endDate) {
-    const formattedStartDate = formatDateString(startDate);
-    const formattedEndDate = formatDateString(endDate);
-
-    const startDateObj = new Date(formattedStartDate);
-    const endDateObj = new Date(formattedEndDate);
-
-    if (startDateObj > endDateObj) {
-      return {
-        startDate: 'Startdatum kann nicht größer sein als Enddatum',
-        endDate: 'Enddatum kann nicht kleiner als Startdatum sein',
-      };
-    }
-  }
-  return true;
-});
+const validateStartDateRange = (value: string) => {
+  const startDate = writableOffer.value?.startDate;
+  const endDate = writableOffer.value?.endDate;
+  if (startDate && endDate) {
+    const startDateObj = new Date(formatDateString(startDate));
+    const endDateObj = new Date(formatDateString(endDate));
+    if (startDateObj > endDateObj) {
+      return 'Startdatum kann nicht größer sein als Enddatum';
+    }
+  }
+  return true;
+};
+
+const validateEndDateRange = (value: string) => {
+  const startDate = writableOffer.value?.startDate;
+  const endDate = writableOffer.value?.endDate;
+  if (startDate && endDate) {
+    const startDateObj = new Date(formatDateString(startDate));
+    const endDateObj = new Date(formatDateString(endDate));
+    if (startDateObj > endDateObj) {
+      return 'Enddatum kann nicht kleiner als Startdatum sein';
+    }
+  }
+  return true;
+};

Similarly, adjust validateDateFields:

-const validateDateFields = computed(() => {
-  const startDate = writableOffer.value?.startDate;
-  const endDate = writableOffer.value?.endDate;
-
-  if ((startDate && !endDate) || (!startDate && endDate)) {
-    return 'Beide Felder müssen ausgefüllt werden, oder beide können gelöscht werden';
-  }
-  return true;
-});
+const validateDateFields = (value: string) => {
+  const startDate = writableOffer.value?.startDate;
+  const endDate = writableOffer.value?.endDate;
+  if ((startDate && !endDate) || (!startDate && endDate)) {
+    return 'Beide Felder müssen ausgefüllt werden, oder beide können gelöscht werden';
+  }
+  return true;
+};

Update the validation rules in your template:

<v-text-field
  v-model="writableOffer.startDate"
  color="secondary"
  label="Startdatum"
- :rules="[validateDateRange.startDate, validateDateFields]"
+ :rules="[validateStartDateRange, validateDateFields]"
  prepend-icon="mdi-calendar"
  clearable
  v-bind="attrs"
  v-on="on"
/>

<v-text-field
  v-model="writableOffer.endDate"
  color="secondary"
  label="Enddatum"
- :rules="[validateDateRange.endDate, validateDateFields]"
+ :rules="[validateEndDateRange, validateDateFields]"
  prepend-icon="mdi-calendar"
  clearable
  v-bind="attrs"
  v-on="on"
/>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    const validateStartDateRange = (value: string) => {
      const startDate = writableOffer.value?.startDate;
      const endDate = writableOffer.value?.endDate;
      if (startDate && endDate) {
        const startDateObj = new Date(formatDateString(startDate));
        const endDateObj = new Date(formatDateString(endDate));
        if (startDateObj > endDateObj) {
          return 'Startdatum kann nicht größer sein als Enddatum';
        }
      }
      return true;
    };

    const validateEndDateRange = (value: string) => {
      const startDate = writableOffer.value?.startDate;
      const endDate = writableOffer.value?.endDate;
      if (startDate && endDate) {
        const startDateObj = new Date(formatDateString(startDate));
        const endDateObj = new Date(formatDateString(endDate));
        if (startDateObj > endDateObj) {
          return 'Enddatum kann nicht kleiner als Startdatum sein';
        }
      }
      return true;
    };

    const validateDateFields = (value: string) => {
      const startDate = writableOffer.value?.startDate;
      const endDate = writableOffer.value?.endDate;
      if ((startDate && !endDate) || (!startDate && endDate)) {
        return 'Beide Felder müssen ausgefüllt werden, oder beide können gelöscht werden';
      }
      return true;
    };

512-567: 🛠️ Refactor suggestion

Use Markdown syntax in applyFormatting function

The applyFormatting function inserts HTML tags directly, but since you're using marked to parse Markdown, it's better to use Markdown syntax for consistency and easier maintenance.

Update the applyFormatting function to use Markdown syntax:

switch (format) {
  case 'bold':
-   newText = `<b>${selectedText}</b>`;
+   newText = `**${selectedText}**`;
    break;
  case 'italic':
-   newText = `<i>${selectedText}</i>`;
+   newText = `*${selectedText}*`;
    break;
  case 'underline':
-   newText = `<u>${selectedText}</u>`;
+   newText = `<u>${selectedText}</u>`; // Markdown doesn't support underline, so HTML is acceptable here
    break;
  case 'ordered-list':
-   newText = `\n<ol>\n<li>${selectedText}</li>\n<li></li>\n<li></li>\n</ol>\n`;
+   newText = `\n1. ${selectedText}\n2. \n3. \n`;
    break;
  case 'unordered-list':
-   newText = `\n<ul>\n<li>${selectedText}</li>\n<li></li>\n<li></li>\n</ul>\n`;
+   newText = `\n- ${selectedText}\n- \n- \n`;
    break;
  case 'line-break':
-   newText = '<br />';
+   newText = `\n`;
    break;
  case 'h1':
-   newText = `<h1>${selectedText}</h1>`;
+   newText = `# ${selectedText}`;
    break;
  case 'h2':
-   newText = `<h2>${selectedText}</h2>`;
+   newText = `## ${selectedText}`;
    break;
  case 'h3':
-   newText = `<h3>${selectedText}</h3>`;
+   newText = `### ${selectedText}`;
    break;
  case 'h4':
-   newText = `<h4>${selectedText}</h4>`;
+   newText = `#### ${selectedText}`;
    break;
  case 'h5':
-   newText = `<h5>${selectedText}</h5>`;
+   newText = `##### ${selectedText}`;
    break;
  default:
    newText = selectedText;
    break;
}

Also, utilize the descriptionTextarea ref to avoid direct DOM manipulation:

const applyFormatting = (format: string) => {
- const textarea = document.querySelector('#description-textarea') as HTMLTextAreaElement | null;
+ const textarea = descriptionTextarea.value;
  if (!textarea) return;

  const start = textarea.selectionStart;
  const end = textarea.selectionEnd;
  // ... rest of the function
};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    const applyFormatting = (format: string) => {
      const textarea = descriptionTextarea.value;
      if (!textarea) return;

      const start = textarea.selectionStart;
      const end = textarea.selectionEnd;
      const selectedText = textarea.value.substring(start, end);

      let newText = '';
      switch (format) {
        case 'bold':
          newText = `**${selectedText}**`;
          break;
        case 'italic':
          newText = `*${selectedText}*`;
          break;
        case 'underline':
          newText = `<u>${selectedText}</u>`; // Markdown doesn't support underline, so HTML is acceptable here
          break;
        case 'ordered-list':
          newText = `\n1. ${selectedText}\n2. \n3. \n`;
          break;
        case 'unordered-list':
          newText = `\n- ${selectedText}\n- \n- \n`;
          break;
        case 'line-break':
          newText = `\n`;
          break;
        case 'h1':
          newText = `# ${selectedText}`;
          break;
        case 'h2':
          newText = `## ${selectedText}`;
          break;
        case 'h3':
          newText = `### ${selectedText}`;
          break;
        case 'h4':
          newText = `#### ${selectedText}`;
          break;
        case 'h5':
          newText = `##### ${selectedText}`;
          break;
        default:
          newText = selectedText;
          break;
      }

      const currentValue = writableOffer.value?.description || '';
      const newValue =
        currentValue.substring(0, start) + newText + currentValue.substring(end);
      writableOffer.value = {
        ...writableOffer.value,
        description: newValue
      } as Offer;
    };

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants